iT邦幫忙

2023 iThome 鐵人賽

DAY 14
0

在上一篇中我們已經將 Django 的專案建立起來了,也新增了一個空的部落格應用程式,接下來是使用 Django ORM 來建立資料庫 Schema,首先先來看一個簡單的部落格應用程式資料庫可能會有哪些資料表,在這邊我使用實體關係圖(Entity Relationship Diagram)來做展示:

https://ithelp.ithome.com.tw/upload/images/20230929/20161957cVLOIrucxg.png

在圖上最右邊的auth_user是 Django 內建的 auth 應用程式的使用者資料表,auth 應用程式本身是個完整的認證與授權模組,它內建還有其他的資料表,這邊為了聚焦在部落格應用本身,所以只顯示使用者資料表,在部落格應用程式上,一定會有文章資料表,如圖上blog_post,而每篇文章都會有作者,作者(author_id)關聯到使用者資料表,然後每篇文章可能會有多個標籤,會有標籤資料表,如圖上blog_tag,文章跟標籤在資料庫的表示是多對多關係,所以會有一張中間的資料表blog_post_tags來儲存多對多關係的資料,在這邊還會有文章的分類,如圖上blog_category,文章可能會有多個分類,所以也是多對多關係,如圖上blog_post_categories,然後會看到分類會有自我關聯,如圖上parent_id欄位,用來標示分類會有階層關係,最後是文章的留言,如圖上blog_comment資料表,留言一樣也會有多個階層的留言。

下面我們開始使用 Django 建立上面的資料表,打開檔案server/app/blog/models.py新增以下內容:

from django.db import models

from django.conf import settings

from server.utils.django.models import BaseModel

USER_MODEL = settings.AUTH_USER_MODEL

class Post(BaseModel):
    slug = models.SlugField("網址代稱", max_length=255, unique=True)
    author = models.ForeignKey(
        USER_MODEL,
        verbose_name="作者",
        on_delete=models.CASCADE,
    )
    title = models.CharField("標題", max_length=255)
    content = models.TextField("內文")
    published_at = models.DateTimeField("發布時間", null=True, blank=True)
    published = models.BooleanField("是否發布", default=False)
    tags = models.ManyToManyField(
        "Tag",
        verbose_name="標籤",
        blank=True,
        related_name="posts",
    )
    categories = models.ManyToManyField(
        "Category",
        verbose_name="分類",
        blank=True,
        related_name="posts",
    )

    def __str__(self) -> str:
        return self.title

    class Meta:
        verbose_name = "文章"
        verbose_name_plural = "文章"
        ordering = ["-created_at"]

class Comment(BaseModel):
    post = models.ForeignKey(
        Post,
        verbose_name="文章",
        on_delete=models.CASCADE,
    )
    parent = models.ForeignKey(
        "self",
        verbose_name="上層留言",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
    )
    author = models.ForeignKey(
        USER_MODEL,
        verbose_name="作者",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
    )
    content = models.TextField("內文")

    def __str__(self) -> str:
        return self.content

    class Meta:
        verbose_name = "留言"
        verbose_name_plural = "留言"
        ordering = ["-created_at"]

class Tag(BaseModel):
    name = models.CharField("標籤名稱", max_length=255, unique=True)

    def __str__(self) -> str:
        return self.name

    class Meta:
        verbose_name = "標籤"
        verbose_name_plural = "標籤"
        ordering = ["-created_at"]

class Category(BaseModel):
    slug = models.SlugField("網址代稱", max_length=255, unique=True)
    parent = models.ForeignKey(
        "self",
        verbose_name="上層分類",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
    )
    name = models.CharField("分類名稱", max_length=255)

		def __str__(self) -> str:
				if self.parent:
            return f"{self.parent}/{self.name}"
        return self.name

    class Meta:
        verbose_name = "分類"
        verbose_name_plural = "分類"
        ordering = ["-created_at"]

前面可以看到定義USER_MODEL,它是使用 Django settingsAUTH_USER_MODEL 設定使用者資料的模型,如果是自定義的使用者模型這種方式是比較好的 [1]。

在 Django 中使用ManyToManyField預設會幫我們建立多對多關係的中間表。

前面提到的自我關聯,在 Django 是使用"self"關鍵字來做到,另外在設計 Django ORM 模型時盡量以物件導向的思維來命名欄位名稱,像是parent,Django 在將模型轉換成資料表時會自動將資料欄位命名成parent_id

新增完成我們需要的 Django 模型後,需要先產生資料庫遷移檔,然後才能透過資料庫遷移檔進行資料庫的異動。

我們先確定我們在django-graphql-tutorial目錄下,先執行poetry shell進到這個專案的 Python 虛擬環境,接著執行下面 Django 指令程式:

$ python manage.py makemigrations

會出現No changes detected訊息,會覺得很奇怪,明明有新增這麼多個 Django 模型,怎麼會沒反應,這是因為忘記將我們部落格應用註冊進 Django 了。

在這邊使用的 Django 專案目錄結構與大部分的教學有所不同,需要對應用程式本身做一些額外設定,需要修改server/app/blog/apps.py

from django.apps import AppConfig

class BlogConfig(AppConfig):
    default_auto_field = "django.db.models.BigAutoField"
    name = "blog"
-   name = "blog"
+   name = "server.app.blog"
+   verbose_name = "部落格"

name要改成應用程式的路徑,並且加上verbose_name之後在 Django admin 可以看到這邊設定的應用程式名稱。

設定完應用程式後,就到server/settings.py中,找到INSTALLED_APPS

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
+	"server.app.blog",
]

註冊我們應用程式模組路徑到 Django 設定中。

接著我們在下一次建立資料庫遷移檔的指令:

$ python manage.py makemigrations
Migrations for 'blog':
  server/app/blog/migrations/0001_initial.py
    - Create model Category
    - Create model Tag
    - Create model Post
    - Create model Comment

指令完成訊息會顯示建立的遷移檔,以及該遷移檔執行步驟的摘要,再接下來,我們要讓 Django 執行遷移檔內資料庫異動步驟,執行以下指令:

$ python manage.py migrate                 
Operations to perform:
  Apply all migrations: admin, auth, blog, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying blog.0001_initial... OK
  Applying sessions.0001_initial... OK

新的 Django 專案,第一次執行資料庫遷移,會執行許多內建應用程式的資料庫異動。

前面的 Django 實作 GraphQL 的練習會先做資料查詢,所以我們先使用 Django admin 作為簡易的資料管理後台,這邊是在server/app/blog/admin.py中新增內容:

from django.contrib import admin

from server.app.blog.models import Category, Comment, Post, Tag

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ["title", "author", "published_at", "published"]
    list_filter = ["published_at", "published"]
    search_fields = ["title"]
    autocomplete_fields = ["author", "tags", "categories"]
    
    
@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
    list_display = ["post", "author", "parent", "content"]
    list_filter = ["post"]
    autocomplete_fields = ["post", "author"]
    

@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
    list_display = ["name"]
    search_fields = ["name"]

@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display = ["name"]
    search_fields = ["name"]

這邊將模型都註冊到 Django admin,並且在各自的 admin 類別稍微做一些設定:

  • list_display:列表頁面要顯示哪些欄位。
  • list_filter:列表頁面右邊篩選可以使用哪些欄位作為篩選項目。
  • search_fields:列表頁面上面搜尋列,可以搜尋哪些欄位的資料。
  • autocomplete_fields:新增或編輯頁面中,表單中關聯的欄位有自動完成的功能。

做到這邊,我們就可以先啟動 Django 開發伺服器了,執行下面指令:

$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
Django version 4.2.5, using settings 'server.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

執行後,可以看到類似以上執行結果,我們就可以根據訊息,打開瀏覽器,瀏覽 Django admin 頁面http://127.0.0.1:8000/admin

https://ithelp.ithome.com.tw/upload/images/20230929/201619577Ie2X14KTn.png

會看到 Django admin 登入頁面,這時候還沒有帳號密碼,我們開啟新的終端機(terminal) session,進入 Python 虛擬環境,在專案下執行以下指令:

$ python manage.py createsuperuser

我們根據指令操作提示完成操作後,就可以再次登入 Django admin 頁面。

https://ithelp.ithome.com.tw/upload/images/20230929/20161957nGuProVQ9h.png

只有我們新增的應用程式是顯示中文,我可以再到server/settings.py做些設定:

-LANGUAGE_CODE = "en-us"
+LANGUAGE_CODE = "zh-hant"

-TIME_ZONE = "UTC"
+TIME_ZONE = "Asia/Taipei"

上面設定語言代碼,以及時區。

存檔後,再回到 Django admin 頁面重新整理,就會到全中文的畫面:

https://ithelp.ithome.com.tw/upload/images/20230929/201619574yFrT3GqTb.png

最後我們就可以先在 Django 上新增一些資料:

https://ithelp.ithome.com.tw/upload/images/20230929/20161957j2rJyyMrVu.png

https://ithelp.ithome.com.tw/upload/images/20230929/20161957o1hmn1ZH1a.png

https://ithelp.ithome.com.tw/upload/images/20230929/201619572aE9OxADnX.png

最後我們要執行程式碼相關格式檢查前,在pyproject.toml中加入忽略migrations目錄下內容的設定:

# ... 忽略

[tool.ruff]
# ... 忽略
+exclude = [
+  "**/migrations/*",
+]

# ... 忽略

+[tool.black]
+extend-exclude = '''
+/(
+  | migrations
+)/
+'''

接下來分別執行:

$ ruff check --fix .
$ pyright .
$ black .

這次修改內容可以參考 Git commit https://github.com/JiaWeiXie/django-graphql-tutorial/commit/b2502305be8f9a26f6aa600b6955e569b0c3c6b3

參考資料


上一篇
Day 13:建立 Django 環境
下一篇
Day 15:Strawberry Django 定義型態與查詢
系列文
Django 與 Strawberry GraphQL:探索現代 API 開發之路30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言